<?php
###
#  Baing PHP 框架 1.0.0
#  http://baing.co/
#
#  Copyright: 白晓阳
#  License: CC BY-NC-SA 4.0
#  https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh
###

namespace bai\action;

use bai\feature\Agent;
use bai\resource\document\DataTable;


/**
 * Example:
 * [
 *    'name' => 'field name',
 *    'type' => 'validate type',
 *    'required' => true,
 *    'length' => 9
 * ]
 */
class Validate extends \bai\Action
{
    public function run()
    {
        $failure = null;
        $fields = $this->find('fields');
        $values = $this->find('values');

        if ($fields) {
            if ($values) {
                $failure = $this->validateAll($fields, $values);
            }
            goto _END;
        }

        $resource = $this->find('resource') ?? self::$process->resource;
        if (!$resource) goto _END;
        $table = DataTable::setup(['path' => $resource]);
        $table->run();

        if ($values) {
            $fields = $table->filterFields(array_keys($values));
            $failure = $this->validateAll($fields, $values);
            goto _END;
        }

        if ($values = self::$request->data) {
            $fields = $table->filterFields(array_keys($values));
            $failure = $this->validateAll($fields, $values);
        }
        if (!$failure && ($query = self::$request->query)) {
            $values = [];
            foreach ($query as $key => $value) {
                if (is_array($value) && isset($value['value'])) {
                    $values[$key] = $value['value'];
                    continue;
                }
                $values[$key] = $value;
            }
            $fields = $table->filterFields(array_keys($values));
            $failure = $this->validateAll($fields, $values, true);
        }

        _END:
        if ($failure) return $this->exit($failure);
        return $this->next();
    }

    protected function validateAll(array $fields, array $values, bool $query = false): ?array
    {
        $method = $query ? self::GET : $this->runtime['method'] ?? self::$request->method;
        foreach ($fields as $key => $field) {
            if (!isset($field['type'])) continue;
            $fieldValue = $values[$key] ?? null;
            if ($failure = $this->validateRequired($fieldValue, $field)) {
                $required = $field['required'] ?? [];
                if ($required !== true && empty($required[$method])) continue;
                $failure['field'] = $key;
                return $failure;
            }

            $call = 'validate' . ucfirst($field['type']);
            if (!method_exists($this, $call)) continue;
            if ($failure = $this->$call($fieldValue, $field, $key)) {
                $failure['field'] = $key;
                return $failure;
            }
        }
        return null;
    }

    protected function validateRequired($value, array $data): ?array
    {
        if ($value === null || $value === '' || $value === []) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateInt($value, array $data): ?array
    {
        $real = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        if (isset($data['min']) && $real < $data['min']) {
            return $this->fail("{$data['type']}.min", ['min' => $data['min']]);
        }
        if (isset($data['max']) && $real > $data['max']) {
            return $this->fail("{$data['type']}.max", ['max' => $data['max']]);
        }
        if (isset($data['length']) && abs($real) >= 10 ** $data['length']) {
            return $this->fail("{$data['type']}.length", ['length' => $data['length']]);
        }
        return null;
    }

    protected function validateFloat($value, array $data): ?array
    {
        $real = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        if (isset($data['min']) && $real < $data['min']) {
            return $this->fail("{$data['type']}.min", ['min' => $data['min']]);
        }
        if (isset($data['max']) && $real > $data['max']) {
            return $this->fail("{$data['type']}.max", ['max' => $data['max']]);
        }
        if (isset($data['length']) && is_array($data['length'])) {
            $int = floor(abs($real));
            $decimal = abs($real) % 1;
            if ($int >= 10 ** $data['length'][0] || strlen(strval($decimal)) - 2 > end($data['length'])) {
                return $this->fail("{$data['type']}.length", ['length' => $data['length']]);
            }
        }
        return null;
    }

    protected function validateBool($value, array $data): ?array
    {
        $real = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateEnum($value, array $data): ?array
    {
        $options = $data['options'] ?? [];
        if (!in_array($value, $options)) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateSet($value, array $data): ?array
    {
        $options = $data['options'] ?? [];
        if (array_diff($value, $options)) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateDate($value, array $data): ?array
    {
        static $format = 'YYYY-MM-DD';

        $real = \DateTime::createFromFormat($format, $value);
        if (!$real) {
            return $this->fail($data['type']);
        }
        if (isset($data['min']) && $real < new \DateTime($data['min'])) {
            return $this->fail("{$data['type']}.min", ['min' => $data['min']]);
        }
        if (isset($data['max']) && $real > new \DateTime($data['max'])) {
            return $this->fail("{$data['type']}.max", ['max' => $data['max']]);
        }
        return null;
    }

    protected function validateTime($value, array $data): ?array
    {
        static $format = 'HH:MM:SS';

        $real = \DateTime::createFromFormat($format, $value);
        if (!$real) {
            return $this->fail($data['type']);
        }
        if (isset($data['min']) && $real < \DateTime::createFromFormat($format, $data['min'])) {
            return $this->fail("{$data['type']}.min", ['min' => $data['min']]);
        }
        if (isset($data['max']) && $real > \DateTime::createFromFormat($format, $data['max'])) {
            return $this->fail("{$data['type']}.max", ['max' => $data['max']]);
        }
        return null;
    }

    protected function validateDatetime($value, array $data): ?array
    {
        static $format = 'YYYY-MM-DD HH:MM:SS';

        $real = \DateTime::createFromFormat($format, $value);
        if (!$real) {
            return $this->fail($data['type']);
        }
        if (isset($data['min']) && $real < new \DateTime($data['min'])) {
            return $this->fail("{$data['type']}.min", ['min' => $data['min']]);
        }
        if (isset($data['max']) && $real > new \DateTime($data['max'])) {
            return $this->fail("{$data['type']}.max", ['max' => $data['max']]);
        }
        return null;
    }

    protected function validateTimestamp($value, array $data): ?array
    {
        return $this->validateDatetime($value, $data);
    }

    protected function validateText($value, array $data): ?array
    {
        if (!is_string($value)) {
            return $this->fail($data['type']);
        }
        if (isset($data['length']) && strlen($value) > $data['length']) {
            return $this->fail("{$data['type']}.length", ['length' => $data['length']]);
        }
        if (isset($data['min']) && strlen($value) < $data['min']) {
            return $this->fail("{$data['type']}.min", ['min' => $data['min']]);
        }
        return null;
    }

    protected function validateString($value, array $data): ?array
    {
        return $this->validateText($value, $data);
    }

    protected function validateJson($value, array $data): ?array
    {
        $real = json_decode($value, true);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateEmail($value, array $data): ?array
    {
        $real = filter_var($value, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        if (isset($data['length']) && strlen($real) > $data['length']) {
            return $this->fail("{$data['type']}.length", ['length' => $data['length']]);
        }
        return null;
    }

    protected function validateUrl($value, array $data): ?array
    {
        $real = filter_var($value, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        if (isset($data['length']) && strlen($real) > $data['length']) {
            return $this->fail("{$data['type']}.length", ['length' => $data['length']]);
        }
        return null;
    }

    protected function validateIp($value, array $data): ?array
    {
        $real = filter_var($value, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE);
        if ($real === null) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateTel($value, array $data): ?array
    {
        $pattern = '/^(?:+[0-9]{1,4})?[0-9-]{6,14}$/';
        if ($this->validateRegex($value, ['pattern' => $pattern])) {
            return $this->fail($data['type']);
        }
        if (isset($data['length']) && strlen($value) > $data['length']) {
            return $this->fail("{$data['type']}.length", ['length' => $data['length']]);
        }
        return null;
    }

    protected function validateRegex($value, array $data): ?array
    {
        $pattern = $data['pattern'] ?? '';
        if (!$pattern || !is_string($value) || !preg_match($pattern, $value)) {
            return $this->fail('regex', $data);
        }
        return null;
    }

    protected function validateFile($value, array $data): ?array
    {
        static $list = [
            'k' => 1024,
            'K' => 1024,
            'm' => 1024 * 1024,
            'M' => 1024 * 1024,
            'g' => 1024 * 1024 * 1024,
            'G' => 1024 * 1024 * 1024,
        ];

        $real = Agent::file($value);
        if (!$real || empty($real['tmp_name']) || $real['error'] !== UPLOAD_ERR_OK) {
            return $this->fail($data['type']);
        }
        if ($ext = $data['ext'] ?? []) {
            if (is_string($real['tmp_name'])) {
                if (!($pos = strrpos($real['tmp_name'], '.', 1))) {
                    return $this->fail("{$data['type']}.ext", ['ext' => $data['ext']]);
                }
                $realExt = strtolower(substr($real['tmp_name'], $pos + 1));
                if (!in_array($realExt, $ext)) {
                    return $this->fail("{$data['type']}.ext", ['ext' => $data['ext']]);
                }
            }
            if (is_array($real['tmp_name'])) {
                foreach ($real['tmp_name'] as $fileName) {
                    if (!($pos = strrpos($fileName, '.', 1))) {
                        return $this->fail("{$data['type']}.ext", ['ext' => $data['ext']]);
                    }
                    $realExt = strtolower(substr($fileName, $pos + 1));
                    if (!in_array($realExt, $ext)) {
                        return $this->fail("{$data['type']}.ext", ['ext' => $data['ext']]);
                    }
                }
            }
        }
        if ($size = $data['size'] ?? 0) {
            if (is_string($size)) {
                $unit = $size[strlen($size) - 1];
                $unit = $list[$unit] ?? 0;
                if ($unit) {
                    $size *= $unit;
                }
            }
            $realSize = is_array($real['size']) ? array_sum($real['size']) : $real['size'];
            if ($realSize > $size) {
                return $this->fail("{$data['type']}.size", ['size' => $data['size']]);
            }
        }
        return null;
    }

    protected function validateUnique($value, array $data, string $name): ?array
    {
        $resource = $data['resource'] ?? '';
        if (!$resource) {
            return $this->fail($data['type']);
        }

        $runtime = [
            'method' => self::GET,
            'resource' => $resource,
            'query' => [$name => $value],
            'quality' => ['LIMIT' => 1],
        ];
        $context = $this->runAction($this->find('DataAccess') ?? 'DataAccess', $runtime);
        if ($context['records']) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validateExist($value, array $data, string $name): ?array
    {
        if ($this->validateUnique($value, $data, $name)) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function validatePassword($value, array $data, string $name): ?array
    {
        $user = Agent::session('user');
        if (!is_array($user) || !password_verify($value, $user[$name] ?? '')) {
            return $this->fail($data['type']);
        }
        return null;
    }

    protected function fail(string $message, ?array $params = null): array
    {
        $failure = ['message' => self::className() . ".$message"];
        if ($params) {
            $failure['params'] = $params;
        }
        return $failure;
    }
}
